Skip to content

Conversation

tcannonfodder
Copy link

What does this PR do?

Add view_component integration for Datadog::Tracing

  • ViewComponent supports instrumentation through ActiveSupport notifications, and having the ability to trace a ViewComponent using Datadog's APM provides a similar level of debugging ease that tracing ActionView template and partial rendering provides
  • This tracing component was based on Datadog::Tracing::Contrib::ActionView, with some key differences:
    • There is only one action available for instrumentation: render
    • ViewComponent is technically its own component, not part of Rails itself
      • Therefore, it uses Contrib::Integration#auto_instrument? rather than checking the Railtie
    • The span.resource is the name of the component (MyComponent), not its identifier (my_component.rb), for a better user experience

WIP: Add view_component build matricies in appraisal

  • I was not able to get the docker compose setup running on my machine due to the inability to pull the ddapm-test-agent
    • Therefore, I was not able to spool up containers for each version of Ruby to generate their gemfiles/
  • While I tested this locally with ruby-3.3, I did not commit the changes since I was not able to follow the containerized setup

Motivation:

I'm using ViewComponent in my apps, and wanted to be able to debug performance issues inside of a ViewComponent.

Change log entry

Additional Notes:

Example screenshot:

image

How to test the change?

Tests have been added based on the action_view integration

* `ViewComponent` supports instrumentation through `ActiveSupport`
	notifications, and having the ability to trace a ViewComponent
	using Datadog's APM provides a similar level of debugging ease that
	tracing `ActionView` template and partial rendering provides
* This tracing component was based on
	`Datadog::Tracing::Contrib::ActionView`, with some key differences:
	* There is only one action available for instrumentation: `render`
	* ViewComponent is technically its own component, not part of Rails
		itself
		* Therefore, it uses `Contrib::Integration#auto_instrument?`
			rather than checking the Railtie
	* The `span.resource` is the name of the component
		(`MyComponent`), not its identifier (`my_component.rb`), for a
		better  user experience
* Added tests, but need to update the Matrixfile and appraisals
@tcannonfodder tcannonfodder requested review from a team as code owners October 14, 2025 11:49
@tcannonfodder tcannonfodder requested a review from vpellan October 14, 2025 11:49
@github-actions github-actions bot added integrations Involves tracing integrations tracing labels Oct 14, 2025
Copy link
Member

@y9v y9v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcannonfodder thank you for your contribution!

I left some small remarks, and I think it would make sense to add an integration test for this feature. If you need some help with the integration test, please let me know.

end

option :service_name
option :component_base_path do |o|
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would components_base_path be better? Since it's a path containing multiple components.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think we would need to ensure the correct format here - when calculating the component identifier, we are relying that this path will end with /, otherwise component identifiers will start with a /

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both this (and the comment below) are essentially straight ports from the action_view integration; which felt like the right approach to take here because these components are run together (you often render ViewComponents inside ActionView), so it feels right that they are configured and behave similarly.

But! Y’all have the final say here; so just lemme know what makes sense for the repo. :) I just wanted to explain my reasoning for these decisions.

References: https://github.com/DataDog/dd-trace-rb/blob/master/lib/datadog/tracing/contrib/action_view/configuration/settings.rb#L34-L37

sections_view = identifier.split(base_path)

if sections_view.length == 1
identifier.split('/')[-1]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this won't work if the components base path is repeated somewhere in the filename:

> 'some/path/components/admin/components/sidebar_component.rb'.split('components/')[-1]
=> "sidebar_component.rb"

@tcannonfodder
Copy link
Author

I left some small remarks, and I think it would make sense to add an integration test for this feature. If you need some help with the integration test, please let me know.

Thanks so much for reviewing it! Eager to get this merged in 😄 . I left some additional contextual comments for why I’d made those decisions; just let me know which approaches you’d like me to take.

I’ll try writing an integration test; and I’ll make a new comment with a WIP commit if I get stuck.

Copy link
Member

@Strech Strech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏🏼 Well done. I think with a few fixes from the comments section it's GTG

end

def subscriptions
all.collect(&:subscriptions).collect(&:to_a).flatten
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could be a flat_map with a single iteration.

Copy link
Member

@y9v y9v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tcannonfodder thank you for your contribution!

@tcannonfodder
Copy link
Author

I haven't forgotten the integration test; just got swamped this weekend! 😵‍💫 Will get that done this week 💪

@github-actions github-actions bot added the core Involves Datadog core libraries label Oct 22, 2025
@tcannonfodder
Copy link
Author

tcannonfodder commented Oct 22, 2025

@Strech, @y9v I figured out what was happening here (configuration changes in ViewComponent + needing the right incantations to prepare a Rails test app for the test suite). I am keeping the old comment for posterity; but the integration test has been written.

Once we're good with it, I can make the other code changes suggested in the comments!


Outdated comment below

I tried my best to get an integration test prepped & ready, but ran into configuration/buildchain issues at the last mile.

See the following commits:

In short: if I try running the test task, I get the following error:

bundle exec rake test:view_component
BUNDLE_GEMFILE=/practical-computer/dd-trace-rb/gemfiles/ruby_3.4_view_component_2.34.0.gemfile bundle check || bundle install && bundle exec rake spec:view_component
Fetching gem metadata from https://rubygems.org/...........
Resolving dependencies...
The Gemfile's dependencies are satisfied
/home/.rbenv/versions/3.4.7/bin/ruby -I/home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/rspec-core-3.13.6/lib:/home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/rspec-support-3.13.6/lib /home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/rspec-core-3.13.6/exe/rspec --pattern spec/datadog/tracing/contrib/view_component/\*\*/\*_spec.rb
/home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/activesupport-6.1.7.10/lib/active_support/dependencies.rb:299: warning: benchmark was loaded from the standard library, but will no longer be part of the default gems starting from Ruby 3.5.0.
You can add benchmark to your Gemfile or gemspec to silence this warning.
Also please contact the author of activesupport-6.1.7.10 to request adding benchmark into its gemspec.
I, [2025-10-21T22:27:07.422096 #15588]  INFO -- : Testing against Rails 6.1.7.10 with adapter 'postgres://postgres:[email protected]:5432/postgres'
Run options: include {focus: true}

All examples were filtered out; ignoring {focus: true}

Randomized with seed 19902
............../home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/activerecord-6.1.7.10/lib/active_record/database_configurations/connection_url_resolver.rb:40: warning: URI::RFC3986_PARSER.unescape is obsolete. Use URI::RFC2396_PARSER.unescape explicitly.
/home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/activerecord-6.1.7.10/lib/active_record/database_configurations/connection_url_resolver.rb:40: warning: URI::RFC3986_PARSER.unescape is obsolete. Use URI::RFC2396_PARSER.unescape explicitly.
/home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/activerecord-6.1.7.10/lib/active_record/database_configurations/connection_url_resolver.rb:40: warning: URI::RFC3986_PARSER.unescape is obsolete. Use URI::RFC2396_PARSER.unescape explicitly.
/home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/activerecord-6.1.7.10/lib/active_record/database_configurations/connection_url_resolver.rb:40: warning: URI::RFC3986_PARSER.unescape is obsolete. Use URI::RFC2396_PARSER.unescape explicitly.
/home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/activerecord-6.1.7.10/lib/active_record/database_configurations/connection_url_resolver.rb:40: warning: URI::RFC3986_PARSER.unescape is obsolete. Use URI::RFC2396_PARSER.unescape explicitly.
F

Failures:

  1) ViewComponent integration tests stores instrumentation data when rendering
     Got 0 failures and 2 other errors:

     1.1) Failure/Error: config.view_component.instrumentation_enabled = true
          
          NoMethodError:
            undefined method 'view_component' for an instance of Rails::Application::Configuration
          # ./spec/datadog/tracing/contrib/view_component/integration_test_spec.rb:13:in 'block (3 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/rails6.rb:40:in 'BasicObject#instance_eval'
          # ./spec/datadog/tracing/contrib/rails/support/rails6.rb:40:in 'block (3 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/base.rb:43:in 'block (3 levels) in <top (required)>'
          # ./spec/support/log_helpers.rb:10:in 'LogHelpers.without_warnings'
          # ./spec/support/log_helpers.rb:3:in 'LogHelpers#without_warnings'
          # ./spec/datadog/tracing/contrib/rails/support/base.rb:41:in 'block (2 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/view_component/integration_test_spec.rb:40:in 'block (2 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/base.rb:70:in 'block (3 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/base.rb:69:in 'block (2 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/log_configuration.rb:7:in 'block (2 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/support/tracer_helpers.rb:96:in 'block (2 levels) in <module:TracerHelpers>'
          # ./spec/spec_helper.rb:272:in 'block (2 levels) in <top (required)>'
          # ./spec/spec_helper.rb:154:in 'block (2 levels) in <top (required)>'
          # ./spec/support/execute_in_fork.rb:32:in 'ForkableExample#run'

     1.2) Failure/Error: config.view_component.instrumentation_enabled = true
          
          NoMethodError:
            undefined method 'view_component' for an instance of Rails::Application::Configuration
          # ./spec/datadog/tracing/contrib/view_component/integration_test_spec.rb:13:in 'block (3 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/rails6.rb:40:in 'BasicObject#instance_eval'
          # ./spec/datadog/tracing/contrib/rails/support/rails6.rb:40:in 'block (3 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/rails6.rb:183:in 'RSpec::ExampleGroups::ViewComponentIntegrationTests#reset_rails_configuration!'
          # ./spec/datadog/tracing/contrib/rails/support/rails6.rb:145:in 'block (2 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/base.rb:70:in 'block (3 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/base.rb:69:in 'block (2 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/rails/support/log_configuration.rb:7:in 'block (2 levels) in <top (required)>'
          # ./spec/datadog/tracing/contrib/support/tracer_helpers.rb:96:in 'block (2 levels) in <module:TracerHelpers>'
          # ./spec/spec_helper.rb:272:in 'block (2 levels) in <top (required)>'
          # ./spec/spec_helper.rb:154:in 'block (2 levels) in <top (required)>'
          # ./spec/support/execute_in_fork.rb:32:in 'ForkableExample#run'

Finished in 0.12837 seconds (files took 0.78248 seconds to load)
15 examples, 1 failure

Failed examples:

rspec ./spec/datadog/tracing/contrib/view_component/integration_test_spec.rb:43 # ViewComponent integration tests stores instrumentation data when rendering

However, running the individual command has the tests pass:

/home/.rbenv/versions/3.4.7/bin/ruby -I/home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/rspec-core-3.13.6/lib:/home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/rspec-support-3.13.6/lib /home/.rbenv/versions/3.4.7/lib/ruby/gems/3.4.0/gems/rspec-core-3.13.6/exe/rspec --pattern spec/datadog/tracing/contrib/view_component/\*\*/\*_spec.rb

Run options: include {focus: true}

All examples were filtered out; ignoring {focus: true}

Randomized with seed 52567
................

Finished in 0.77797 seconds (files took 1.59 seconds to load)
15 examples, 0 failures

Thanks so much for your help!

* Older versions of ViewComponent had a different notification name
	for ActiveSupport notifications, `!render.view_component`
	* See: ViewComponent/view_component#1771
* To support this, there is a configuration flag,
	`:use_deprecated_instrumentation_name`, which mirrors the name of
	the configuration option within `ViewComponent` itself
	* If set to true, it changes the `event_name` for the
		`ViewComponent::Events::Render#event_name` to be
		`!render.view_component`
* I was not able to get the `docker compose` setup running on my
	machine due to the inability to pull the `ddapm-test-agent`
	* Therefore, I was not able to spool up containers for each version
		of Ruby to generate their `gemfiles/`
* While I tested this locally with `ruby-3.3`, I did not commit the
	changes since I was not able to follow the containerized setup
* Followed the documentation to add `DD_TRACE_VIEW_COMPONENT_ENABLED`
	to `supported-configurations.json`, then run the task to generate
	`lib/datadog/core/configuration/supported_configurations.rb`
* In order to write working integration tests for the major
	versions of ViewComponent that have instrumentation support, we need
	to use custom appraisal sets instead of `build_coverage_matrix`,
	since an ad-hoc Rails app needs to be spooled up, which includes
	`pg`, `rails`, and `sprockets < 4` for the max versions of Rails for
	`ViewComponent` `2.34.0
* I was not able to get the `docker compose` setup running on my
	machine due to the inability to pull the `ddapm-test-agent`
	* Therefore, I was not able to spool up containers for each version
		of Ruby to generate their `gemfiles/`
* While I tested this locally with `ruby-3.3`, I did not commit the
	changes since I was not able to follow the containerized setup
* In order to properly write an integration test for ViewComponent
	we need to spin up an ad-hoc Rails app that we render the component
	within, then confirm the span data is generated
* The integration test includes the
	`datadog/tracing/contrib/rails/rails_helper`, which provides a
	`Rails test application` context
* Since there were configuration changes between different versions
	of ViewComponent, we need to adjust the configuration settings
	based on which version of the Gem has been loaded
	* This can be done using `Gem.loaded_specs` and `Gem::Version`,
		which provide an API for version comparison
	* Testing across the Appraised versions also checks that the
		`use_deprecated_instrumentation_name` configuration is applied,
		and does collect the necessary data
@tcannonfodder tcannonfodder force-pushed the tcannonfodder/add-view_component-tracing branch from a4ba6e0 to 1e2b4c7 Compare October 22, 2025 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core Involves Datadog core libraries integrations Involves tracing integrations tracing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants